﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.VisualStudio.Debugger.Interop;
using Red.CommUtils;
using Red.Scripts.Debugger.Defines;

namespace Red.Scripts.Debugger.Remote.Debugger
{
    //TODO: Move to comm protocol (Currently it's duplicated across engine and debugger)
    enum Attributes
    {
        /// Has children
        IsExpandable = 0,

        // Cannot be edited by a debugger
        ReadOnly,

        Abstract,
        Constant,

        // Value is not valid
        Invalid,

        // Access type
        AccessPublic,
        AccessPrivate,
        AccessProtected,

        // Is a boolean value
        TypeBoolean,

        // Value can be represented by a string
        TypeString,

        // Some other type of value
        TypeProperty,

        TypeClass,
        TypeBaseClass,
        TypeInnerClass,
        TypeMostDerivedClass
    };

    static class AttributesExtensions
    {
        public static bool IsInFlags< T >( this Attributes attr, T flags ) where T : IConvertible
        {
            uint attrFlags = ( 1u << (int)attr );
            return Utility.HasFlag( attrFlags, flags );
        }
    }

    class Property : IDebugProperty2
    {
        #region Public Properties

        /// <summary>
        /// Override the name supplied by the engine
        /// </summary>
        public string Name { get; set; }

        #endregion Public Properties

        #region Private Data

        private uint _threadId;
        private int _stackId;
        private comm.ScriptLocal _data;
        private IChannel _channel;

        #endregion Private Data

        #region C-Tor

        public Property( comm.ScriptLocal local, IChannel channel, uint threadId, int frameId )
        {
            _threadId = threadId;
            _stackId = frameId;
            _data = local;
            _channel = channel;
        }

        #endregion C-Tor

        #region Public Functions

        public static DEBUG_PROPERTY_INFO[] Enumerate( IList<comm.ScriptLocal> locals, IChannel channel, enum_DEBUGPROP_INFO_FLAGS dwFields, uint threadId, int frameId )
        {
            DEBUG_PROPERTY_INFO[] properties = new DEBUG_PROPERTY_INFO[ locals.Count ];

            for ( int i = 0; i < locals.Count; ++i )
            {
                comm.ScriptLocal local = locals[ i ];
                Property property = new Property( local, channel, threadId, frameId );
                DEBUG_PROPERTY_INFO info = property.ConstructDebugPropertyInfo( dwFields );

                properties[ i ] = info;
            }

            return properties;
        }

        #endregion Public Functions

        #region Private Methods

        private DEBUG_PROPERTY_INFO ConstructDebugPropertyInfo( enum_DEBUGPROP_INFO_FLAGS dwFields )
        {
            Debug.Assert( _data != null );

            DEBUG_PROPERTY_INFO propertyInfo = new DEBUG_PROPERTY_INFO();

            if ( Utility.HasFlag( dwFields, enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_FULLNAME ) )
            {
                propertyInfo.bstrFullName = Name ?? _data.path;
                propertyInfo.dwFields |= enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_FULLNAME;
            }

            if ( Utility.HasFlag( dwFields, enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_NAME ) )
            {
                propertyInfo.bstrName = Name ?? _data.name;
                propertyInfo.dwFields |= enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_NAME;
            }

            if ( Utility.HasFlag( dwFields, enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_TYPE ) )
            {
                propertyInfo.bstrType = _data.type;
                propertyInfo.dwFields |= enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_TYPE;
            }

            if ( Utility.HasFlag( dwFields, enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_VALUE ) )
            {
                propertyInfo.bstrValue = _data.value;
                propertyInfo.dwFields |= enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_VALUE;
            }

            if ( Utility.HasFlag( dwFields, enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_ATTRIB ) )
            {
                if ( Attributes.IsExpandable.IsInFlags( _data.attributes ) )
                    propertyInfo.dwAttrib |= enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_OBJ_IS_EXPANDABLE;

                if ( Attributes.ReadOnly.IsInFlags( _data.attributes ) )
                    propertyInfo.dwAttrib |= enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_VALUE_READONLY;

                if ( Attributes.Abstract.IsInFlags( _data.attributes ) )
                    propertyInfo.dwAttrib |= enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_INTERFACE;

                if ( Attributes.Constant.IsInFlags( _data.attributes ) )
                    propertyInfo.dwAttrib |= enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_TYPE_CONSTANT;

                if ( Attributes.Invalid.IsInFlags( _data.attributes ) )
                    propertyInfo.dwAttrib |= enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_VALUE_INVALID;

                if ( Attributes.AccessPublic.IsInFlags( _data.attributes ) )
                    propertyInfo.dwAttrib |= enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_ACCESS_PUBLIC;

                if ( Attributes.AccessPrivate.IsInFlags( _data.attributes ) )
                    propertyInfo.dwAttrib |= enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_ACCESS_PRIVATE;

                if ( Attributes.AccessProtected.IsInFlags( _data.attributes ) )
                    propertyInfo.dwAttrib |= enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_ACCESS_PROTECTED;

                if ( Attributes.TypeBoolean.IsInFlags( _data.attributes ) )
                    propertyInfo.dwAttrib |= enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_VALUE_BOOLEAN;

                if ( Attributes.TypeString.IsInFlags( _data.attributes ) )
                    propertyInfo.dwAttrib |= enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_VALUE_RAW_STRING;

                if ( Attributes.TypeProperty.IsInFlags( _data.attributes ) )
                    propertyInfo.dwAttrib |= enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_PROPERTY;

                if ( Attributes.TypeClass.IsInFlags( _data.attributes ) )
                    propertyInfo.dwAttrib |= enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_CLASS;

                if ( Attributes.TypeBaseClass.IsInFlags( _data.attributes ) )
                    propertyInfo.dwAttrib |= enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_BASECLASS;

                if ( Attributes.TypeInnerClass.IsInFlags( _data.attributes ) )
                    propertyInfo.dwAttrib |= enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_INNERCLASS;

                if ( Attributes.TypeMostDerivedClass.IsInFlags( _data.attributes ) )
                    propertyInfo.dwAttrib |= enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_MOSTDERIVEDCLASS;

                propertyInfo.dwFields |= enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_ATTRIB;
            }

            if ( Utility.HasFlag( dwFields, enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_VALUE_AUTOEXPAND ) )
            {
                propertyInfo.dwAttrib |= enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_VALUE_AUTOEXPANDED;
                propertyInfo.dwFields |= enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_ATTRIB;
            }

            if ( Utility.HasFlag( dwFields, enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_PROP ) )
            {
                propertyInfo.pProperty = this;
                propertyInfo.dwFields |= enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_PROP;
            }

            return propertyInfo;
        }

        #endregion Private Methods

        #region IDebugProperty2

        public int EnumChildren( enum_DEBUGPROP_INFO_FLAGS dwFields, uint dwRadix, ref Guid guidFilter, enum_DBG_ATTRIB_FLAGS dwAttribFilter, string pszNameFilter, uint dwTimeout, out IEnumDebugPropertyInfo2 ppEnum )
        {
            comm.ScriptLocalsRequest request = new comm.ScriptLocalsRequest();
            request.path = _data.path;
            request.stackId = _stackId;
            request.threadId = _threadId;

            Debug.Print( $"Sending locals request for \"{_data.path}\"" );

            // Temp so that we don't timeout while I've got the engine stopped at a breakpoint
            if( System.Diagnostics.Debugger.IsAttached )
                dwTimeout = 5000000;

            comm.ScriptLocals response = null;

            try
            {
                response = _channel.SendAndWait( request, (int)dwTimeout ) as comm.ScriptLocals;
            }
            catch ( CommunicationTimeoutException )
            {
            	
            }

            if ( response == null )
            {
                Debug.Print( $"No response received for locals request: \"{_data.path}\"" );

                ppEnum = null;
                return Constants.S_FALSE;
            }

            Debug.Print( $"Response received for locals request: \"{_data.path}\"" );

            DEBUG_PROPERTY_INFO[] properties = Enumerate( response.locals, _channel, dwFields, _threadId, _stackId );
            ppEnum = new Defines.Enumerators.Property( properties );

            return Constants.S_OK;
        }

        public int GetDerivedMostProperty( out IDebugProperty2 ppDerivedMost )
        {
            throw new NotImplementedException();
        }

        public int GetExtendedInfo( ref Guid guidExtendedInfo, out object pExtendedInfo )
        {
            throw new NotImplementedException();
        }

        public int GetMemoryBytes( out IDebugMemoryBytes2 ppMemoryBytes )
        {
            throw new NotImplementedException();
        }

        public int GetMemoryContext( out IDebugMemoryContext2 ppMemory )
        {
            ppMemory = null;

            return Constants.S_OK;
        }

        public int GetParent( out IDebugProperty2 ppParent )
        {
            throw new NotImplementedException();
        }

        public int GetPropertyInfo( enum_DEBUGPROP_INFO_FLAGS dwFields, uint dwRadix, uint dwTimeout, IDebugReference2[] rgpArgs, uint dwArgCount, DEBUG_PROPERTY_INFO[] pPropertyInfo )
        {
            rgpArgs = null;
            pPropertyInfo[ 0 ] = ConstructDebugPropertyInfo( dwFields );

            return Constants.S_OK;
        }

        public int GetReference( out IDebugReference2 ppReference )
        {
            throw new NotImplementedException();
        }

        public int GetSize( out uint pdwSize )
        {
            throw new NotImplementedException();
        }

        public int SetValueAsReference( IDebugReference2[] rgpArgs, uint dwArgCount, IDebugReference2 pValue, uint dwTimeout )
        {
            throw new NotImplementedException();
        }

        public int SetValueAsString( string pszValue, uint dwRadix, uint dwTimeout )
        {
            throw new NotImplementedException();
        }

        #endregion IDebugProperty2
    }
}
